package com.hqd.ch03.v51.context.event;

/*
 * Copyright 2002-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


import com.hqd.ch03.utils.ClassUtils;
import com.hqd.ch03.utils.ObjectUtils;
import com.hqd.ch03.utils.ReflectionUtils;
import com.hqd.ch03.v51.context.ApplicationContext;
import com.hqd.ch03.v51.context.ApplicationEvent;
import com.hqd.ch03.v51.context.PayloadApplicationEvent;
import com.hqd.ch03.v51.core.BridgeMethodResolver;
import com.hqd.ch03.v51.core.Ordered;
import com.hqd.ch03.v51.core.ResolvableType;
import com.hqd.ch03.v51.core.annotation.AnnotatedElementUtils;
import com.hqd.ch03.v51.core.annotation.Order;
import com.hqd.ch03.v51.utils.AopUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.*;

public class ApplicationListenerMethodAdapter implements GenericApplicationListener {

    private static final boolean reactiveStreamsPresent = ClassUtils.isPresent(
            "org.reactivestreams.Publisher", ApplicationListenerMethodAdapter.class.getClassLoader());


    protected final Log logger = LogFactory.getLog(getClass());

    private final String beanName;

    private final Method method;

    private final Method targetMethod;


    private final List<ResolvableType> declaredEventTypes;


    private final String condition;

    private final int order;


    private volatile String listenerId;


    private ApplicationContext applicationContext;


    /**
     * Construct a new ApplicationListenerMethodAdapter.
     *
     * @param beanName    the name of the bean to invoke the listener method on
     * @param targetClass the target class that the method is declared on
     * @param method      the listener method to invoke
     */
    public ApplicationListenerMethodAdapter(String beanName, Class<?> targetClass, Method method) {
        this.beanName = beanName;
        this.method = BridgeMethodResolver.findBridgedMethod(method);
        this.targetMethod = (!Proxy.isProxyClass(targetClass) ?
                AopUtils.getMostSpecificMethod(method, targetClass) : this.method);

        EventListener ann = AnnotatedElementUtils.findMergedAnnotation(this.targetMethod, EventListener.class);
        this.declaredEventTypes = resolveDeclaredEventTypes(method, ann);
        this.condition = (ann != null ? ann.condition() : null);
        this.order = resolveOrder(this.targetMethod);
        String id = (ann != null ? ann.id() : "");
        this.listenerId = (!id.isEmpty() ? id : null);
    }

    private static List<ResolvableType> resolveDeclaredEventTypes(Method method, EventListener ann) {
        int count = method.getParameterCount();
        if (count > 1) {
            throw new IllegalStateException(
                    "Maximum one parameter is allowed for event listener method: " + method);
        }

        if (ann != null) {
            Class<?>[] classes = ann.classes();
            if (classes.length > 0) {
                List<ResolvableType> types = new ArrayList<>(classes.length);
                for (Class<?> eventType : classes) {
                    types.add(ResolvableType.forClass(eventType));
                }
                return types;
            }
        }

        if (count == 0) {
            throw new IllegalStateException(
                    "Event parameter is mandatory for event listener method: " + method);
        }
        return Collections.singletonList(ResolvableType.forMethodParameter(method, 0));
    }

    private static int resolveOrder(Method method) {
        Order ann = AnnotatedElementUtils.findMergedAnnotation(method, Order.class);
        return (ann != null ? ann.value() : Ordered.LOWEST_PRECEDENCE);
    }


    /**
     * Initialize this instance.
     */
    void init(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }


    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        processEvent(event);
    }

    @Override
    public boolean supportsEventType(ResolvableType eventType) {
        for (ResolvableType declaredEventType : this.declaredEventTypes) {
            if (declaredEventType.isAssignableFrom(eventType)) {
                return true;
            }
            if (PayloadApplicationEvent.class.isAssignableFrom(eventType.toClass())) {
                ResolvableType payloadType = eventType.as(PayloadApplicationEvent.class).getGeneric();
                if (declaredEventType.isAssignableFrom(payloadType)) {
                    return true;
                }
            }
        }
        return eventType.hasUnresolvableGenerics();
    }

    @Override
    public boolean supportsSourceType(Class<?> sourceType) {
        return true;
    }

    @Override
    public int getOrder() {
        return this.order;
    }

    @Override
    public String getListenerId() {
        String id = this.listenerId;
        if (id == null) {
            id = getDefaultListenerId();
            this.listenerId = id;
        }
        return id;
    }

    /**
     * Determine the default id for the target listener, to be applied in case of
     * no {@link EventListener#id() annotation-specified id value}.
     * <p>The default implementation builds a method name with parameter types.
     *
     * @see #getListenerId()
     * @since 5.3.5
     */
    protected String getDefaultListenerId() {
        Method method = getTargetMethod();
        StringJoiner sj = new StringJoiner(",", "(", ")");
        for (Class<?> paramType : method.getParameterTypes()) {
            sj.add(paramType.getName());
        }
        return ClassUtils.getQualifiedMethodName(method) + sj.toString();
    }


    /**
     * Process the specified {@link ApplicationEvent}, checking if the condition
     * matches and handling a non-null result, if any.
     */
    public void processEvent(ApplicationEvent event) {
        Object[] args = resolveArguments(event);
        if (shouldHandle(event, args)) {
            Object result = doInvoke(args);
            if (result != null) {
                // handleResult(result);
            } else {
                logger.trace("No result object given - no result to handle");
            }
        }
    }

    /**
     * Resolve the method arguments to use for the specified {@link ApplicationEvent}.
     * <p>These arguments will be used to invoke the method handled by this instance.
     * Can return {@code null} to indicate that no suitable arguments could be resolved
     * and therefore the method should not be invoked at all for the specified event.
     */

    protected Object[] resolveArguments(ApplicationEvent event) {
        ResolvableType declaredEventType = getResolvableType(event);
        if (declaredEventType == null) {
            return null;
        }
        if (this.method.getParameterCount() == 0) {
            return new Object[0];
        }
        Class<?> declaredEventClass = declaredEventType.toClass();
        if (!ApplicationEvent.class.isAssignableFrom(declaredEventClass) &&
                event instanceof PayloadApplicationEvent) {
            Object payload = ((PayloadApplicationEvent<?>) event).getPayload();
            if (declaredEventClass.isInstance(payload)) {
                return new Object[]{payload};
            }
        }
        return new Object[]{event};
    }


    private void publishEvents(Object result) {
        if (result.getClass().isArray()) {
            Object[] events = ObjectUtils.toObjectArray(result);
            for (Object event : events) {
                publishEvent(event);
            }
        } else if (result instanceof Collection<?>) {
            Collection<?> events = (Collection<?>) result;
            for (Object event : events) {
                publishEvent(event);
            }
        } else {
            publishEvent(result);
        }
    }

    private void publishEvent(Object event) {
        if (event != null) {
            this.applicationContext.publishEvent(event);
        }
    }

    protected void handleAsyncError(Throwable t) {
        logger.error("Unexpected error occurred in asynchronous listener", t);
    }

    private boolean shouldHandle(ApplicationEvent event, Object[] args) {
        if (args == null) {
            return false;
        }
        return true;
    }

    /**
     * Invoke the event listener method with the given argument values.
     */

    protected Object doInvoke(Object... args) {
        Object bean = getTargetBean();
        // Detect package-protected NullBean instance through equals(null) check
        if (bean.equals(null)) {
            return null;
        }

        ReflectionUtils.makeAccessible(this.method);
        try {
            return this.method.invoke(bean, args);
        } catch (IllegalArgumentException ex) {
            assertTargetBean(this.method, bean, args);
            throw new IllegalStateException(getInvocationErrorMessage(bean, ex.getMessage(), args), ex);
        } catch (IllegalAccessException ex) {
            throw new IllegalStateException(getInvocationErrorMessage(bean, ex.getMessage(), args), ex);
        } catch (InvocationTargetException ex) {
            // Throw underlying exception
            Throwable targetException = ex.getTargetException();
            if (targetException instanceof RuntimeException) {
                throw (RuntimeException) targetException;
            } else {
                String msg = getInvocationErrorMessage(bean, "Failed to invoke event listener method", args);
                throw new UndeclaredThrowableException(targetException, msg);
            }
        }
    }

    /**
     * Return the target bean instance to use.
     */
    protected Object getTargetBean() {
        return this.applicationContext.getBean(this.beanName);
    }

    /**
     * Return the target listener method.
     *
     * @since 5.3
     */
    protected Method getTargetMethod() {
        return this.targetMethod;
    }

    /**
     * Return the condition to use.
     * <p>Matches the {@code condition} attribute of the {@link EventListener}
     * annotation or any matching attribute on a composed annotation that
     * is meta-annotated with {@code @EventListener}.
     */

    protected String getCondition() {
        return this.condition;
    }

    /**
     * Add additional details such as the bean type and method signature to
     * the given error message.
     *
     * @param message error message to append the HandlerMethod details to
     */
    protected String getDetailedErrorMessage(Object bean, String message) {
        StringBuilder sb = new StringBuilder(message).append('\n');
        sb.append("HandlerMethod details: \n");
        sb.append("Bean [").append(bean.getClass().getName()).append("]\n");
        sb.append("Method [").append(this.method.toGenericString()).append("]\n");
        return sb.toString();
    }

    /**
     * Assert that the target bean class is an instance of the class where the given
     * method is declared. In some cases the actual bean instance at event-
     * processing time may be a JDK dynamic proxy (lazy initialization, prototype
     * beans, and others). Event listener beans that require proxying should prefer
     * class-based proxy mechanisms.
     */
    private void assertTargetBean(Method method, Object targetBean, Object[] args) {
        Class<?> methodDeclaringClass = method.getDeclaringClass();
        Class<?> targetBeanClass = targetBean.getClass();
        if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) {
            String msg = "The event listener method class '" + methodDeclaringClass.getName() +
                    "' is not an instance of the actual bean class '" +
                    targetBeanClass.getName() + "'. If the bean requires proxying " +
                    "(e.g. due to @Transactional), please use class-based proxying.";
            throw new IllegalStateException(getInvocationErrorMessage(targetBean, msg, args));
        }
    }

    private String getInvocationErrorMessage(Object bean, String message, Object[] resolvedArgs) {
        StringBuilder sb = new StringBuilder(getDetailedErrorMessage(bean, message));
        sb.append("Resolved arguments: \n");
        for (int i = 0; i < resolvedArgs.length; i++) {
            sb.append('[').append(i).append("] ");
            if (resolvedArgs[i] == null) {
                sb.append("[null] \n");
            } else {
                sb.append("[type=").append(resolvedArgs[i].getClass().getName()).append("] ");
                sb.append("[value=").append(resolvedArgs[i]).append("]\n");
            }
        }
        return sb.toString();
    }


    private ResolvableType getResolvableType(ApplicationEvent event) {
        ResolvableType payloadType = null;
        if (event instanceof PayloadApplicationEvent) {
            PayloadApplicationEvent<?> payloadEvent = (PayloadApplicationEvent<?>) event;
            ResolvableType eventType = payloadEvent.getResolvableType();
            if (eventType != null) {
                payloadType = eventType.as(PayloadApplicationEvent.class).getGeneric();
            }
        }
        for (ResolvableType declaredEventType : this.declaredEventTypes) {
            Class<?> eventClass = declaredEventType.toClass();
            if (!ApplicationEvent.class.isAssignableFrom(eventClass) &&
                    payloadType != null && declaredEventType.isAssignableFrom(payloadType)) {
                return declaredEventType;
            }
            if (eventClass.isInstance(event)) {
                return declaredEventType;
            }
        }
        return null;
    }


    @Override
    public String toString() {
        return this.method.toGenericString();
    }


}
